{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# 在 bare metal Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 上运行 TVM\n",
        "\n",
        "**原作者**：[Grant Watson](https://github.com/grant-arm)\n",
        "\n",
        "本节包含如何在 bare metal 使用 TVM 在 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 上运行模型的例子。Cortex(R)-M55 是小型、低功耗的 CPU，专为嵌入式设备设计。Ethos(TM)-U55 是 microNPU，专门用于加速资源有限的嵌入式设备中的 ML 推理。\n",
        "\n",
        "为了运行演示应用程序，无需访问 Cortex(R)-M55 和 Ethos(TM)-U55 开发板，将在固定的虚拟平台（Fixed Virtual Platform，简称 FVP）上运行示例应用程序。FVP 基于 Arm(R) Corstone(TM)-300 软件，模型的硬件系统包含 Cortex(R)-M55 和 Ethos(TM)-U55。它提供了适合于软件开发的程序员的视图。\n",
        "\n",
        "在本教程中，将编译 MobileNet v1 模型，并指示 TVM 在可能的情况下将算子卸载到 Ethos(TM)-U55。\n",
        "\n",
        "## 获取 TVM\n",
        "\n",
        "为您的平台获取 TVM，请访问 <https://tlcpack.ai/> 并遵循说明。一旦正确安装了 TVM，您应该可以从命令行访问 ``tvmc``。\n",
        "\n",
        "在命令行输入 ``tvmc`` 应该显示如下：\n",
        "\n",
        "```\n",
        "usage: tvmc [-h] [-v] [--version] {tune,compile,run} ...\n",
        "\n",
        "    TVM compiler driver\n",
        "\n",
        "    optional arguments:\n",
        "      -h, --help          show this help message and exit\n",
        "      -v, --verbose       increase verbosity\n",
        "      --version           print the version and exit\n",
        "\n",
        "    commands:\n",
        "      {tune,compile,run}\n",
        "        tune              auto-tune a model\n",
        "        compile           compile a model.\n",
        "        run               run a compiled module\n",
        "\n",
        "  TVMC - TVM driver command-line interface\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 安装附加的 Python 依赖项\n",
        "\n",
        "为了运行演示，您将需要一些额外的 Python 包。这些可以通过使用需求来安装。以下文件：\n",
        "\n",
        "```\n",
        ":caption: requirements.txt\n",
        ":name: requirements.txt\n",
        "\n",
        "attrs==21.2.0\n",
        "cloudpickle==2.0.0\n",
        "decorator==5.1.0\n",
        "ethos-u-vela==3.2.0\n",
        "flatbuffers==1.12\n",
        "lxml==4.6.3\n",
        "nose==1.3.7\n",
        "numpy==1.19.5\n",
        "Pillow==8.3.2\n",
        "psutil==5.8.0\n",
        "scipy==1.5.4\n",
        "synr==0.4\n",
        "tflite==2.4.0\n",
        "tornado==6.1\n",
        "```\n",
        "\n",
        "这些包可以通过在命令行运行以下命令来安装：\n",
        "\n",
        "```bash\n",
        "pip install -r requirements.txt\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 获得模型\n",
        "\n",
        "在本教程中，将使用 MobileNet v1。MobileNet v1 是用于对图像进行分类的卷积神经网络，已经针对边缘设备进行了优化。将使用的模型已经经过了预训练，可以将图像分类为 1001 个不同的类别之一。该网络的输入图像大小为 224x224，所以任何输入的图像在使用前都需要调整到这些尺寸。\n",
        "\n",
        "在本教程中，将使用 Tflite 格式的模型。\n",
        "\n",
        "```bash\n",
        "mkdir -p ./build\n",
        "cd build\n",
        "wget https://storage.googleapis.com/download.tensorflow.org/models/mobilenet_v1_2018_08_02/mobilenet_v1_1.0_224_quant.tgz\n",
        "gunzip mobilenet_v1_1.0_224_quant.tgz\n",
        "tar xvf mobilenet_v1_1.0_224_quant.tar\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 为 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 编译模型\n",
        "\n",
        "一旦下载了 MobileNet v1 模型，下一步就是编译它。为此，将使用 `tvmc` 编译。从编译过程中得到的输出是编译为目标平台的模型库格式（MLF）的模型的 TAR 包。将能够使用 TVM 运行时在目标设备上运行该模型。\n",
        "\n",
        "```bash\n",
        "!python -m tvm.driver.tvmc compile --target=\"ethos-u -accelerator_config=ethos-u55-256, c\" \\\n",
        "             --target-c-mcpu=cortex-m55 \\\n",
        "             --runtime=crt \\\n",
        "             --executor=aot \\\n",
        "             --executor-aot-interface-api=c \\\n",
        "             --executor-aot-unpacked-api=1 \\\n",
        "             --pass-config tir.disable_vectorize=1 \\\n",
        "             ./build/mobilenet_v1_1.0_224_quant.tflite \\\n",
        "             --output-format=mlf\n",
        "```\n",
        "\n",
        "```{admonition} tvmc 编译参数的解释\n",
        ":class: alert alert-info\n",
        "\n",
        "* ``--target=\"ethos-u -accelerator_config=ethos-u55-256, c\"`` : offload operators to the Ethos(TM)-U55 NPU where possible and fall back to using generated C code on the Cortex(R)-M where an operator is not supported on the NPU..\n",
        "\n",
        "* ``--target-c-mcpu=cortex-m55`` : Cross-compile for the Cortex(R)-M55.\n",
        "\n",
        "* ``--runtime=crt`` : Generate glue code to allow operators to work with C runtime.\n",
        "\n",
        "* ``--executor=aot`` : Use Ahead Of Time compiltaion instead of the Graph Executor.\n",
        "\n",
        "* ``--executor-aot-interface-api=c`` : Generate a C-style interface with structures designed for integrating into C apps at the boundary.\n",
        "\n",
        "* ``--executor-aot-unpacked-api=1`` : Use the unpacked API internally.\n",
        "\n",
        "* ``--pass-config tir.disable_vectorize=1`` : Disable vectorize since there are no standard vectorized types in C.\n",
        "\n",
        "* ``./mobilenet_v1_1.0_224_quant.tflite`` : The TFLite model that is being compiled.\n",
        "\n",
        "* ``--output-format=mlf`` : Output should be generated in the Model Library Format.\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### 将生成的代码提取到当前目录\n",
        "\n",
        "```bash\n",
        "tar xvf module.tar\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### 获得 ImageNet 标签\n",
        "\n",
        "When running MobileNet v1 on an image, the result is an index in the range 0 to\n",
        "1000. In order to make our application a little more user friendly, instead of\n",
        "just displaying the category index, we will display the associated label. We\n",
        "will download these image labels into a text file now and use a python script\n",
        "to include them in our C application later.\n",
        "\n",
        "```bash\n",
        "curl -sS  https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/java/demo/app/src/main/assets/labels_mobilenet_quant_v1_224.txt \\\n",
        "  -o ./labels_mobilenet_quant_v1_224.txt\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "### Getting the input image\n",
        "\n",
        "As input for this tutorial, we will use the image of a cat, but you can\n",
        "substitute an image of your choosing.\n",
        "\n",
        "```{image} https://s3.amazonaws.com/model-server/inputs/kitten.jpg\n",
        ":height: 224px\n",
        ":width: 224px\n",
        ":align: center\n",
        "```\n",
        "\n",
        "We download the image into the build directory and we will use a python script\n",
        "in the next step to convert the image into an array of bytes in a C header file.\n",
        "\n",
        "```bash\n",
        "curl -sS https://s3.amazonaws.com/model-server/inputs/kitten.jpg -o ./kitten.jpg\n",
        "```\n",
        "\n",
        "### Pre-processing the image\n",
        "\n",
        "The following script will create 2 C header files in the src directory:\n",
        "\n",
        "* ``inputs.h`` - The image supplied as an argument to the script will be converted\n",
        "  to an array of integers for input to our MobileNet v1 model.\n",
        "* ``outputs.h`` - An integer array of zeroes will reserve 1001 integer values\n",
        "  for the output of inference."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        " #!python ./convert_image.py\n",
        " import os\n",
        " import pathlib\n",
        " import re\n",
        " import sys\n",
        " from PIL import Image\n",
        " import numpy as np\n",
        "\n",
        "\n",
        " def create_header_file(name, section, tensor_name, tensor_data, output_path):\n",
        "     \"\"\"\n",
        "     This function generates a header file containing the data from the numpy array provided.\n",
        "     \"\"\"\n",
        "     file_path = pathlib.Path(f\"{output_path}/\" + name).resolve()\n",
        "     # Create header file with npy_data as a C array\n",
        "     raw_path = file_path.with_suffix(\".h\").resolve()\n",
        "     with open(raw_path, \"w\") as header_file:\n",
        "         header_file.write(\n",
        "             \"#include <tvmgen_default.h>\\n\"\n",
        "             + f\"const size_t {tensor_name}_len = {tensor_data.size};\\n\"\n",
        "             + f'uint8_t {tensor_name}[] __attribute__((section(\"{section}\"), aligned(16))) = \"'\n",
        "         )\n",
        "         data_hexstr = tensor_data.tobytes().hex()\n",
        "         for i in range(0, len(data_hexstr), 2):\n",
        "             header_file.write(f\"\\\\x{data_hexstr[i:i+2]}\")\n",
        "         header_file.write('\";\\n\\n')\n",
        "\n",
        "\n",
        " def create_headers(image_name):\n",
        "     \"\"\"\n",
        "     This function generates C header files for the input and output arrays required to run inferences\n",
        "     \"\"\"\n",
        "     img_path = os.path.join(\"./\", f\"{image_name}\")\n",
        "\n",
        "     # Resize image to 224x224\n",
        "     resized_image = Image.open(img_path).resize((224, 224))\n",
        "     img_data = np.asarray(resized_image).astype(\"float32\")\n",
        "\n",
        "     # Convert input to NCHW\n",
        "     img_data = np.transpose(img_data, (2, 0, 1))\n",
        "\n",
        "     # Create input header file\n",
        "     input_data = img_data.astype(np.uint8)\n",
        "     create_header_file(\"inputs\", \"ethosu_scratch\", \"input\", input_data, \"./include\")\n",
        "     # Create output header file\n",
        "     output_data = np.zeros([1001], np.uint8)\n",
        "     create_header_file(\n",
        "         \"outputs\",\n",
        "         \"output_data_sec\",\n",
        "         \"output\",\n",
        "         output_data,\n",
        "         \"./include\",\n",
        "     )\n",
        "\n",
        "\n",
        " if __name__ == \"__main__\":\n",
        "     create_headers(sys.argv[1])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "从命令行运行脚本：\n",
        "\n",
        "```bash\n",
        "python convert_image.py ./kitten.jpg\n",
        "```\n",
        "\n",
        "### Pre-processing the labels\n",
        "\n",
        "The following script will create a ``labels.h`` header file in the src directory.\n",
        "The labels.txt file that we downloaded previously will be turned\n",
        "into an array of strings. This array will be used to display the label that\n",
        "our image has been classified as."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "#!python ./convert_labels.py\n",
        " import os\n",
        " import pathlib\n",
        " import sys\n",
        "\n",
        "\n",
        " def create_labels_header(labels_file, section, output_path):\n",
        "     \"\"\"\n",
        "     This function generates a header file containing the ImageNet labels as an array of strings\n",
        "     \"\"\"\n",
        "     labels_path = pathlib.Path(labels_file).resolve()\n",
        "     file_path = pathlib.Path(f\"{output_path}/labels.h\").resolve()\n",
        "\n",
        "     with open(labels_path) as f:\n",
        "         labels = f.readlines()\n",
        "\n",
        "     with open(file_path, \"w\") as header_file:\n",
        "         header_file.write(f'char* labels[] __attribute__((section(\"{section}\"), aligned(16))) = {{')\n",
        "\n",
        "         for _, label in enumerate(labels):\n",
        "             header_file.write(f'\"{label.rstrip()}\",')\n",
        "\n",
        "         header_file.write(\"};\\n\")\n",
        "\n",
        "\n",
        " if __name__ == \"__main__\":\n",
        "     create_labels_header(sys.argv[1], \"ethosu_scratch\", \"./include\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "从命令行运行脚本：\n",
        "\n",
        "```bash\n",
        "python convert_labels.py\n",
        "```\n",
        "\n",
        "## Writing the demo application\n",
        "\n",
        "The following C application will run a single inference of the MobileNet v1\n",
        "model on the image that we downloaded and converted to an array of integers\n",
        "previously. Since the model was compiled with a target of \"ethos-u ...\",\n",
        "operators supported by the Ethos(TM)-U55 NPU will be offloaded for acceleration.\n",
        "Once the application is built and run, our test image should be correctly\n",
        "classied as a \"tabby\" and the result should be displayed on the console.\n",
        "This file should be placed in ``./src``"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "```c\n",
        " // demo.c\n",
        " #include <stdio.h>\n",
        " #include <tvm_runtime.h>\n",
        "\n",
        " #include \"ethosu_mod.h\"\n",
        " #include \"uart.h\"\n",
        "\n",
        " // Header files generated by convert_image.py and convert_labels.py\n",
        " #include \"inputs.h\"\n",
        " #include \"labels.h\"\n",
        " #include \"outputs.h\"\n",
        "\n",
        " int abs(int v) { return v * ((v > 0) - (v < 0)); }\n",
        "\n",
        " int main(int argc, char** argv) {\n",
        "   uart_init();\n",
        "   printf(\"Starting Demo\\n\");\n",
        "   EthosuInit();\n",
        "\n",
        "   printf(\"Allocating memory\\n\");\n",
        "   StackMemoryManager_Init(&app_workspace, g_aot_memory, WORKSPACE_SIZE);\n",
        "\n",
        "   printf(\"Running inference\\n\");\n",
        "   struct tvmgen_default_outputs outputs = {\n",
        "       .output = output,\n",
        "   };\n",
        "   struct tvmgen_default_inputs inputs = {\n",
        "       .input = input,\n",
        "   };\n",
        "   struct ethosu_driver* driver = ethosu_reserve_driver();\n",
        "   struct tvmgen_default_devices devices = {\n",
        "       .ethos_u = driver,\n",
        "   };\n",
        "   tvmgen_default_run(&inputs, &outputs, &devices);\n",
        "   ethosu_release_driver(driver);\n",
        "\n",
        "   // Calculate index of max value\n",
        "   uint8_t max_value = 0;\n",
        "   int32_t max_index = -1;\n",
        "   for (unsigned int i = 0; i < output_len; ++i) {\n",
        "     if (output[i] > max_value) {\n",
        "       max_value = output[i];\n",
        "       max_index = i;\n",
        "     }\n",
        "   }\n",
        "   printf(\"The image has been classified as '%s'\\n\", labels[max_index]);\n",
        "\n",
        "   // The FVP will shut down when it receives \"EXITTHESIM\" on the UART\n",
        "   printf(\"EXITTHESIM\\n\");\n",
        "   while (1 == 1)\n",
        "     ;\n",
        "   return 0;\n",
        " }\n",
        " ```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "<div class=\"alert alert-info\"><h4>Note</h4><p>If you'd like to use FreeRTOS for task scheduling and queues, a sample application can be found here\n",
        "  `demo_freertos.c <https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/src/demo_freertos.c>`</p></div>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Creating the linker script\n",
        "--------------------------\n",
        "\n",
        "We need to create a linker script that will be used when we build our application\n",
        "in the following section. The linker script tells the linker where everything\n",
        "should be placed in memory. The corstone300.ld linker script below should be\n",
        "placed in your working directory.\n",
        "\n",
        "An example linker script for the FVP can be found here\n",
        "`corstone300.ld <https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/corstone300.ld>`_\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "<div class=\"alert alert-info\"><h4>Note</h4><p>The code generated by TVM will place the model weights and the Arm(R)\n",
        "  Ethos(TM)-U55 command stream in a section named ``ethosu_scratch``.\n",
        "  For a model the size of MobileNet v1, the weights and command stream will not\n",
        "  fit into the limited SRAM available. For this reason it's important that the\n",
        "  linker script places the ``ethosu_scratch`` section into DRAM (DDR).</p></div>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "<div class=\"alert alert-info\"><h4>Note</h4><p>Before building and running the application, you will need to update your\n",
        "  PATH environment variable to include the path to cmake 3.19.5 and the FVP.\n",
        "  For example if you've installed these in ``/opt/arm`` , then you would do\n",
        "  the following:\n",
        "\n",
        "  ``export PATH=/opt/arm/FVP_Corstone_SSE-300_Ethos-U55/models/Linux64_GCC-6.4:/opt/arm/cmake/bin:$PATH``</p></div>\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Building the demo application using make\n",
        "----------------------------------------\n",
        "\n",
        "We can now build the demo application using make. The Makefile should be placed\n",
        "in your working directory before running ``make`` on the command line:\n",
        "\n",
        "An example Makefile can be found here:\n",
        "`Makefile <https://github.com/apache/tvm/blob/main/apps/microtvm/ethosu/Makefile>`_\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "<div class=\"alert alert-info\"><h4>Note</h4><p>If you're using FreeRTOS, the Makefile builds it from the specified FREERTOS_PATH:\n",
        "    ``make FREERTOS_PATH=<FreeRTOS directory>``</p></div>\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Running the demo application\n",
        "----------------------------\n",
        "\n",
        "Finally, we can run our demo appliction on the Fixed Virtual Platform (FVP),\n",
        "by using the following command:\n",
        "\n",
        ".. code-block:: bash\n",
        "\n",
        "    FVP_Corstone_SSE-300_Ethos-U55 -C cpu0.CFGDTCMSZ=15 \\\n",
        "    -C cpu0.CFGITCMSZ=15 -C mps3_board.uart0.out_file=\\\"-\\\" -C mps3_board.uart0.shutdown_tag=\\\"EXITTHESIM\\\" \\\n",
        "    -C mps3_board.visualisation.disable-visualisation=1 -C mps3_board.telnetterminal0.start_telnet=0 \\\n",
        "    -C mps3_board.telnetterminal1.start_telnet=0 -C mps3_board.telnetterminal2.start_telnet=0 -C mps3_board.telnetterminal5.start_telnet=0 \\\n",
        "    -C ethosu.extra_args=\"--fast\" \\\n",
        "    -C ethosu.num_macs=256 ./build/demo\n",
        "\n",
        "You should see the following output displayed in your console window:\n",
        "\n",
        ".. code-block:: text\n",
        "\n",
        "    telnetterminal0: Listening for serial connection on port 5000\n",
        "    telnetterminal1: Listening for serial connection on port 5001\n",
        "    telnetterminal2: Listening for serial connection on port 5002\n",
        "    telnetterminal5: Listening for serial connection on port 5003\n",
        "\n",
        "        Ethos-U rev dedfa618 --- Jan 12 2021 23:03:55\n",
        "        (C) COPYRIGHT 2019-2021 Arm Limited\n",
        "        ALL RIGHTS RESERVED\n",
        "\n",
        "    Starting Demo\n",
        "    ethosu_init. base_address=0x48102000, fast_memory=0x0, fast_memory_size=0, secure=1, privileged=1\n",
        "    ethosu_register_driver: New NPU driver at address 0x20000de8 is registered.\n",
        "    CMD=0x00000000\n",
        "    Soft reset NPU\n",
        "    Allocating memory\n",
        "    Running inference\n",
        "    ethosu_find_and_reserve_driver - Driver 0x20000de8 reserved.\n",
        "    ethosu_invoke\n",
        "    CMD=0x00000004\n",
        "    QCONFIG=0x00000002\n",
        "    REGIONCFG0=0x00000003\n",
        "    REGIONCFG1=0x00000003\n",
        "    REGIONCFG2=0x00000013\n",
        "    REGIONCFG3=0x00000053\n",
        "    REGIONCFG4=0x00000153\n",
        "    REGIONCFG5=0x00000553\n",
        "    REGIONCFG6=0x00001553\n",
        "    REGIONCFG7=0x00005553\n",
        "    AXI_LIMIT0=0x0f1f0000\n",
        "    AXI_LIMIT1=0x0f1f0000\n",
        "    AXI_LIMIT2=0x0f1f0000\n",
        "    AXI_LIMIT3=0x0f1f0000\n",
        "    ethosu_invoke OPTIMIZER_CONFIG\n",
        "    handle_optimizer_config:\n",
        "    Optimizer release nbr: 0 patch: 1\n",
        "    Optimizer config cmd_stream_version: 0 macs_per_cc: 8 shram_size: 48 custom_dma: 0\n",
        "    Optimizer config Ethos-U version: 1.0.6\n",
        "    Ethos-U config cmd_stream_version: 0 macs_per_cc: 8 shram_size: 48 custom_dma: 0\n",
        "    Ethos-U version: 1.0.6\n",
        "    ethosu_invoke NOP\n",
        "    ethosu_invoke NOP\n",
        "    ethosu_invoke NOP\n",
        "    ethosu_invoke COMMAND_STREAM\n",
        "    handle_command_stream: cmd_stream=0x61025be0, cms_length 1181\n",
        "    QBASE=0x0000000061025be0, QSIZE=4724, base_pointer_offset=0x00000000\n",
        "    BASEP0=0x0000000061026e60\n",
        "    BASEP1=0x0000000060002f10\n",
        "    BASEP2=0x0000000060002f10\n",
        "    BASEP3=0x0000000061000fb0\n",
        "    BASEP4=0x0000000060000fb0\n",
        "    CMD=0x000Interrupt. status=0xffff0022, qread=4724\n",
        "    CMD=0x00000006\n",
        "    00006\n",
        "    CMD=0x0000000c\n",
        "    ethosu_release_driver - Driver 0x20000de8 released\n",
        "    The image has been classified as 'tabby'\n",
        "    EXITTHESIM\n",
        "    Info: /OSCI/SystemC: Simulation stopped by user.\n",
        "\n",
        "You should see near the end of the output that the image has been correctly\n",
        "classified as 'tabby'.\n",
        "\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.10.4"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
